{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "initial_id",
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 下载地址：https://pjreddie.com/projects/pascal-voc-dataset-mirror/#google_vignette"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "6ca903b46d3b4d9e"
  },
  {
   "cell_type": "markdown",
   "source": [
    "# IoU 交并比\n",
    "## IOU指标是衡量检测框与目标框的面积交并比。"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "e4dcbb161645a87d"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 坐标轴\n",
    "#  |\n",
    "# -+-----→x\n",
    "#  |\n",
    "#  ↓y\n",
    "# 极坐标点表示方法：(xmin,ymin,xmax,ymax)\n",
    "# 中心点坐标表示方法：(x_center,y_center,w,h)\n",
    "def iou(box1, box2, wh=False):\n",
    "    if wh:\n",
    "        # 中心点坐标表示，获取两个box的中心点\n",
    "        # 第一个框左上角坐标\n",
    "        xmin1, ymin1 = int(box1[0] - box1[2] / 2.0), int(box1[1] - box1[3] / 2.0)\n",
    "        # 第一个框右下角坐标\n",
    "        xmax1, ymax1 = int(box1[0] + box1[2] / 2.0), int(box1[1] + box1[3] / 2.0)\n",
    "        # 第二个框左上角坐标\n",
    "        xmin2, ymin2 = int(box2[0] - box2[2] / 2.0), int(box2[1] - box2[3] / 2.0)\n",
    "        # 第二个框右下角坐标\n",
    "        xmax2, ymax2 = int(box2[0] + box2[2] / 2.0), int(box2[1] + box2[3] / 2.0)\n",
    "    else:\n",
    "        #使用极坐标形式表示，直接获取两个bbox的坐标\n",
    "        xmin1, ymin1, xmax1, ymax1 = box1\n",
    "        xmin2, ymin2, xmax2, ymax2 = box2\n",
    "    # 获取举行框交集对应的左上角和右下角坐标\n",
    "    xx1 = np.max([xmin1, xmin2])\n",
    "    yy1 = np.max([ymin1, ymin2])\n",
    "    xx2 = np.min([xmax2, xmax1])\n",
    "    yy2 = np.min([ymax2, ymax1])\n",
    "\n",
    "    # 计算交集面积\n",
    "    int_area = (np.max([0, xx2 - xx1])) * (np.max([0, yy2 - yy1]))\n",
    "\n",
    "    #计算两个矩形框并集面积\n",
    "    area1 = (xmax1 - xmin1) * (ymax1 - ymin1)\n",
    "    area2 = (xmax2 - xmin2) * (ymax2 - ymin2)\n",
    "    union_area = area1 + area2 - int_area\n",
    "\n",
    "    # 计算交并比,1e-6,防止分母0\n",
    "    return int_area / (union_area + 1e-6)"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "d5ea1772f896baa3"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.patches as patches"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "866d13cedf1358b8"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 测试IOU\n",
    "# 真实坐标，预测坐标\n",
    "true_box = [30, 60, 270, 330]  # (xmin,ymin,xmax,ymax)\n",
    "pre_box = [50, 80, 290, 350]  #(xmin,ymin,xmax,ymax)\n",
    "\n",
    "img = plt.imread(\"./pic/cat.jpg\")\n",
    "\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "77f9d72f9b440924"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "fig = plt.imshow(img)\n",
    "\n",
    "fig.axes.add_patch(plt.Rectangle((true_box[0], true_box[1]),\n",
    "                                 width=true_box[2] - true_box[0],\n",
    "                                 height=true_box[3] - true_box[1],\n",
    "                                 fill=False,\n",
    "                                 edgecolor=\"green\",\n",
    "                                 linewidth=2))\n",
    "\n",
    "fig.axes.add_patch(plt.Rectangle((pre_box[0], pre_box[1]),\n",
    "                                 width=pre_box[2] - pre_box[0],\n",
    "                                 height=pre_box[3] - pre_box[1],\n",
    "                                 fill=False,\n",
    "                                 edgecolor=\"red\",\n",
    "                                 linewidth=2,\n",
    "                                 linestyle=\"dashed\"))\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "55f26d0773f825c2"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "iou(true_box, pre_box)"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "e40eef3279b1c712"
  },
  {
   "cell_type": "markdown",
   "source": [
    "# NMS\n",
    "## 用于减少冗余检测框，直到只有一个检测框"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "4ba98bb977bf1612"
  },
  {
   "cell_type": "code",
   "execution_count": 122,
   "outputs": [],
   "source": [
    "def nms(boxes, score, threshold):\n",
    "    \"\"\"\n",
    "    非极大值抑制，NMS实现过程，\n",
    "    用的是极坐标，不是中心点坐标\n",
    "    :param boxes: 同类别候选框坐标\n",
    "    :param score: 同类别候选框得分\n",
    "    :param threshold: iou阈值，如果值越小，保留的框越少，\n",
    "    :return: 返回经过NMS处理的框坐标和分数\n",
    "    \"\"\"\n",
    "    # 1.如果无候选框则返回空列表\n",
    "    if len(boxes) == 0:\n",
    "        return [], []\n",
    "    # 类型转换\n",
    "    _boxes = np.array(boxes)\n",
    "    _score = np.array(score)\n",
    "\n",
    "    # 取出所有极坐标\n",
    "    x1 = _boxes[:, 0]\n",
    "    y1 = _boxes[:, 1]\n",
    "    x2 = _boxes[:, 2]\n",
    "    y2 = _boxes[:, 3]\n",
    "    # 根据_boxes计算面积，注意是基于_boxes的维度进行计算  \n",
    "    areas = (x2 - x1) * (y2 - y1)  # 使用_boxes的维度来计算面积 \n",
    "\n",
    "    # 2.对候选框进行NMS筛选\n",
    "    # 返回的框坐标和分数\n",
    "    picked_boxes = []\n",
    "    picked_score = []\n",
    "\n",
    "    # 对置信度进行排序，获取排序后的下标序号，np.argsort是默认从小到大排序，返回的是下标\n",
    "    order = np.argsort(_score)  #注意，排的是下标！\n",
    "    while order.size > 0:\n",
    "        # 将当前置信度最大的框加入返回值列表\n",
    "        idx = order[-1]  # 获取最后一个元素，即_score中最大值的下标\n",
    "        # 保留该类剩余的框中，得分最高的一个\n",
    "        picked_boxes.append(_boxes[idx])\n",
    "        picked_score.append(_score[idx])\n",
    "        # IoU 获取当前置信度最大的候选框与其他任意的相交面积 \n",
    "        x11 = np.maximum(x1[idx], x1[order[:-1]])  #np.maximum：a与b逐位比较取其大者；\n",
    "        y11 = np.maximum(y1[idx], y1[order[:-1]])\n",
    "        x22 = np.minimum(x2[idx], x2[order[:-1]])  #np.minimum：a与b逐位比较取其小者；\n",
    "        y22 = np.minimum(y2[idx], y2[order[:-1]])\n",
    "        \n",
    "        # # 计算相交面积，不重叠时的面积为0\n",
    "        # w = np.maximum(0.0, x22 - x11)\n",
    "        # h = np.maximum(0.0, y22 - y11)\n",
    "        # intersection = w * h  # 计算相交面积  \n",
    "        intersection = np.maximum(0, x22 - x11) * np.maximum(0, y22 - y11)\n",
    "        # 利用相交的面积和两个框自身的面积计算框的交并比\n",
    "        iou = intersection / (areas[idx] + areas[order[:-1]] - intersection)  # 这里应当使用areas数组进行计算，确保正确性。  \n",
    "        # 保留IoU小于阈值的box的下标\n",
    "        keep_boxes = np.where(iou < threshold)\n",
    "        # print(keep_boxes, order)\n",
    "        # 保留剩余的框\n",
    "        order = order[keep_boxes]\n",
    "        # print(picked_boxes, picked_score)\n",
    "        # print(\"--\" * 10)\n",
    "    return picked_boxes, picked_score\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-16T13:49:29.544111700Z",
     "start_time": "2024-01-16T13:49:29.507306300Z"
    }
   },
   "id": "ab4cbb9ecafbb0c6"
  },
  {
   "cell_type": "markdown",
   "source": [
    "## MNS使用例子"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "5e04b8a34568e157"
  },
  {
   "cell_type": "code",
   "execution_count": 123,
   "outputs": [],
   "source": [
    "# 一共3个框 \n",
    "bounding = [(187, 82, 337, 317), (150, 67, 305, 282), (246, 121, 368, 304)]\n",
    "# 分别与类别的得分\n",
    "confidence_score = [0.9, 0.65, 0.8, ]\n",
    "# 阈值\n",
    "threshold = 0.5"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-16T13:49:30.538415400Z",
     "start_time": "2024-01-16T13:49:30.513356100Z"
    }
   },
   "id": "4a774a79077a2937"
  },
  {
   "cell_type": "code",
   "execution_count": 124,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[array([187,  82, 337, 317]), array([246, 121, 368, 304])]\n"
     ]
    }
   ],
   "source": [
    "box, score = nms(bounding, confidence_score, threshold)\n",
    "print(box)\n",
    "# print(score)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-01-16T13:49:30.949756400Z",
     "start_time": "2024-01-16T13:49:30.935817200Z"
    }
   },
   "id": "6048bcec6d6fbdb7"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [],
   "metadata": {
    "collapsed": false
   },
   "id": "f7f76f097e4dba9d"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [],
   "metadata": {
    "collapsed": false
   },
   "id": "308ef5cf173885e7"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [],
   "metadata": {
    "collapsed": false
   },
   "id": "a703aecd13ecfcc6"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [],
   "metadata": {
    "collapsed": false
   },
   "id": "c5711fb061e56be3"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "def nms2(boxes, score, threshold):\n",
    "    \"\"\"  \n",
    "    AI写的\n",
    "    非极大值抑制函数  \n",
    "    :param boxes: 候选框坐标列表  \n",
    "    :param score: 对应每个候选框的得分列表  \n",
    "    :param threshold: 交并比阈值  \n",
    "    :return: 经过非极大值抑制处理的框坐标列表和得分列表  \n",
    "    \"\"\"\n",
    "    picked_boxes = []\n",
    "    picked_score = []\n",
    "    order = np.argsort(score)[::-1]  # 按照得分降序排列  \n",
    "    while order.size > 0:\n",
    "        idx = order[0]  # 获取得分最高的候选框的下标  \n",
    "        picked_boxes.append(boxes[idx])\n",
    "        picked_score.append(score[idx])\n",
    "        x1 = np.maximum(0, boxes[idx, 0] - threshold)  # 计算候选框的左边界  \n",
    "        y1 = np.maximum(0, boxes[idx, 1] - threshold)  # 计算候选框的上边界  \n",
    "        x2 = np.minimum(boxes[idx, 2], boxes[idx, 0] + threshold)  # 计算候选框的右边界  \n",
    "        y2 = np.minimum(boxes[idx, 3], boxes[idx, 1] + threshold)  # 计算候选框的下边界  \n",
    "        area = (x2 - x1) * (y2 - y1)  # 计算候选框的面积  \n",
    "        x11 = np.maximum(0, boxes[order[1:], 0] - threshold)  # 计算剩余候选框的左边界  \n",
    "        y11 = np.maximum(0, boxes[order[1:], 1] - threshold)  # 计算剩余候选框的上边界  \n",
    "        x22 = np.minimum(boxes[order[1:], 2], boxes[order[1:], 0] + threshold)  # 计算剩余候选框的右边界  \n",
    "        y22 = np.minimum(boxes[order[1:], 3], boxes[order[1:], 1] + threshold)  # 计算剩余候选框的下边界  \n",
    "        intersection = np.maximum(0, x22 - x11) * np.maximum(0, y22 - y11)  # 计算剩余候选框与当前候选框的交集面积  \n",
    "        iou = intersection / (area + area[order[1:]] - intersection)  # 计算剩余候选框与当前候选框的交并比  \n",
    "        keep = iou <= threshold  # 保留交并比小于阈值的候选框  \n",
    "        order = order[keep]  # 保留交并比小于阈值的候选框的下标，注意这里不需要使用[:-1]  \n",
    "    return picked_boxes, picked_score"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "3da0f5feaadb4d9d"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# 定义候选框坐标和得分  \n",
    "boxes = np.array([[10, 10, 20, 20], [10, 10, 25, 25], [30, 30, 40, 40]])\n",
    "score = np.array([0.9, 0.8, 0.7])\n",
    "\n",
    "# 设置交并比阈值  \n",
    "threshold = 0.5\n",
    "\n",
    "# 调用NMS函数  \n",
    "picked_boxes, picked_score = nms2(boxes, score, threshold)\n"
   ],
   "metadata": {
    "collapsed": false
   },
   "id": "7e10bba1662e2abe"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [],
   "metadata": {
    "collapsed": false
   },
   "id": "2b2d9c9d4504590f"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
